感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
在 React 18 後已經棄用
ReactDOM.render()
,改用ReactDOM.createRoot()
,內文中的圖片並未一併修改,煩請讀者留意。
傳統的網頁開發上,我們會把所有的 CSS 樣式寫在一支或多支的 CSS 檔內,接著在 index.html
中透過 <link rel="stylesheet" href="main.css" />
的方式讓整個網站都能夠套用到這支 CSS 所撰寫的樣式。
上述這種方式因為所有的樣式都是作用在整個網頁的環境下,常會發生不小心命名了同樣的 class
名稱,導致樣式相互影響或彼此覆蓋,又或者發生某些樣式權重不夠的情況而難以調整,因此在 class 的命名上常常需要非常留意小心,進而也出現了許多對於 class 命名的不同規範和設計模式。
然而,現今前端框架中,因為可以把各個元件給拆分開來,每個不同功能的按鈕都可以是不同的元件,每個元件之間可以是獨立不互相干擾的,所以連 CSS 的樣式也都可以有元件的概念存在,也就是說,在某個元件內所撰寫的樣式,即使有相同 class 的命名,但在最後編譯後這些樣式都只會作用在該元件內,不會干擾到外層或其他元件的樣式。
這類把樣式連同元件寫在一起寫法稱作 CSS-in-JS,它的好處在於每個元件都是獨立可被重複使用,你不用再擔心改了 A 卡片的樣式卻不小心連 B 卡片的顏色也變了;你也不用再擔心把以為某支 CSS 檔案是多餘的,砍掉之後卻發生破版的情況,因為現在元件和樣式是綁在一起的,只要這個元件是完整的,那麼放到另一個地方去使用它時,外觀和功能也會是一樣的。
除此之外,既然 CSS 已經被放入的 JS 檔案中,如同把 HTML 寫在 JS 檔中的 JSX 一樣,這些 CSS 的樣式也將可以適用 JS 的語法。
透過把 CSS 寫在 JS 中的這種寫法,可以確保特定樣式只會作用在該元件之外,同時還可以把 JS 中的一些邏輯判斷放到 CSS 使用。
現在,就讓我們來看看怎麼使用帶有樣式的元件吧!
提醒:實務上會同時搭配上述兩種做法,有些樣式仍會撰寫在全域環境,讓整個網頁都可以套用到該樣式(例如、版型、主題、字體、...);針對個別元件則在撰寫只用在該元件內的樣式。
React 中要讓每個元件帶有獨立樣式的做法很多,非常多人選擇使用 styled-components(下圖右側)或 emotion(下圖左側)這兩套工具:
styled-components 是從 2016 年就開始的專案,使用人數和 Github 星星數一直都非常多,而 emotion 則自 2017 年開始,可能是由於新版本的 styled-components 一直停留在 beta 而未正式發表的緣故,許多人開始轉而使用 emotion。
從 npm trends 的分析可以看到雖然 styled-components 仍然擁有最多的星星數,但在 8 月 25 日的時候正式出現死亡交叉,emotion 的下載量正式且持續的超過 styled-components:
? 補充:在挑選套件時,可以透過 npm trends 把幾個具有相似功能的套件進行比較,通常可以看出一些趨勢和找到適合自己的。
實際上這兩個套件的用法上非常接近,Styled-Component 因為比較早起步且非常多人使用,網路上已經可以找到許多教學範例,因此在後面的練習中,我們就來嘗試看看後起之秀的 emotion 吧!
昨天我們已經在 CodeSandbox 開啟了一個全新的 React 專案,現在我們可以直接在 CodeSandbox 的左側點選 Add Dependency 來載入和 Emotion 相關的套件,主要有 @emotion/core 和 @emotion/styled 這兩個:
先在 ./src
資料夾中新增一支名為 WeatherApp.js
的檔案,接著透過函式來建立一個名為WeatherApp
的元件,比較不一樣的地方是現在因為我們把不同的組間拆成不同支 JS 檔撰寫,因此在檔案開頭的地方要透過 import
把 React 載入進來,在最後的地方則要透過 export
把這個元件匯出:
// ./src/WeatherApp.js
import React from 'react';
const WeatherApp = () => {
return <h1>Weather</h1>;
};
export default WeatherApp;
整個畫面會像這樣:
接著到 index.js
這支檔案來把剛剛寫好的 WeatherApp
載入進來,並把多餘不必要的程式碼移除。可以留意到這裡有一句 import './styles.css';
,透過這種方式載入的 CSS 因為沒有縮限在某個元件內,因此是會作用到整個網頁的:
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import WeatherApp from './WeatherApp';
// 這支 CSS 檔的樣式會作用到全域
import './styles.css';
function App() {
return <WeatherApp />;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
整個畫面會像這樣:
也就是說,如果有什麼樣式需要作用到整個網頁的話,就可以寫在 ./src/styles.css
這支檔案中。
這裡為了讓各個 HTML 元素在每個瀏覽器上看起來是一致的,我們先在 CodeSandbox 左下角「Add Resources」的地方貼上 https://unpkg.com/normalize.css@8.0.1/normalize.css
以載入 normalize.css:
接著,為了之後可以把整個版面撐滿,然後再把顯示天氣的區域放在畫面的正中央,因此在 styles.css
的檔案中,可以再加上:
/* ./src/styles.css */
html,
body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
#root {
height: 100%;
width: 100%;
}
接下來,就可以來撰寫 WeatherApp 的版面和樣式了。
打開 ./src/WeatherApp.js
,先來撰寫第一個帶有樣式的 component。
過去在撰寫樣式時我們會像這樣為有需要的 HTML 元素加上 className
來帶入樣式:
const WeatherApp = () => {
return (
<div className="container">
<div className="weather-card">
<h1>Weather</h1>
</div>
</div>
);
};
然後在 CSS 檔的地方去定義 .container
和 .weather-card
的樣式:
.container {
background-color: #ededed;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.weather-card {
min-width: 360px;
box-shadow: 0 1px 3px 0 #999999;
background-color: #f9f9f9;
}
現在當我們要撰寫 CSS-in-js 的寫法時,會像下面的步驟這樣:
import
將 emotion
套件載入<div>
標籤時,只需要使用 styled.div
;如果要建立的是 <button>
則是使用 styled.button
其他則以此類推styled.div
後面加上兩個反引號(和 Template Literals 用的符號相同),在兩個反引號之間就可以 直接撰寫 CSS 。實際上這裡的 styled.div
是一個函式,而在函式後面直接加上反引號一樣屬於 Template Literals 的一種用法,只是比較少情況會這樣使用。import React from 'react';
// STEP 1:載入 emotion 的 styled 套件
import styled from '@emotion/styled';
// STEP 2:定義帶有 styled 的 component
const Container = styled.div`
background-color: #ededed;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
`;
const WeatherCard = styled.div`
position: relative;
min-width: 360px;
box-shadow: 0 1px 3px 0 #999999;
background-color: #f9f9f9;
box-sizing: border-box;
padding: 30px 15px;
`;
// STEP 3:把上面定義好的 styled-component 當成元件使用
const WeatherApp = () => {
return (
<Container>
<WeatherCard>
<h1>Weather</h1>
</WeatherCard>
</Container>
);
};
export default WeatherApp;
若想進一步了解在函式後面直接加上反引號的這種 Template Literal 用法,可以餐考這篇 JavaScript Template Literals and styled-components。
完成的畫面會像這樣:
CodeSandbox 和 CodePen 一樣可以有 Debug 這種頁面方便除錯,只需要複製右邊網頁上方的網址貼在瀏覽器網址列上即可:
在這個頁面打開瀏覽器的開發者工具可以看到,這些帶有樣式的元件,最後都會帶上特殊的 class
名稱,並且套用上所撰寫的 CSS 樣式,而這也就是為什麼不同元件之間的 CSS 樣式不會相互干擾的原因。即使這不同頁面中都定義了一個同樣名為 <Container />
的 styled-component,但因為它們最終會帶上不同的 class 名稱,因此元件間的樣式並不會相互干擾:
今天先讓我們完成到這裡,有需要的話可以到 Weather App - started template @ CodeSandbox 檢視完整的程式碼。明天會使用更多 emotion 來完成樣式並載入 SVG 圖案,把畫面像下面這樣完成,接著再透過中央氣象局的 API 拉取即時的資料來顯示。